FridaFuzz Summary (一)

FridaFuzz总结

起始

您可以使用Notion的内置目录功能来自动生成目录。在左侧边栏中,选择“添加目录”,然后将其拖动到您想要的位置。在文档中添加标题后,目录将自动更新。注意,此功能仅在页面级别上工作,而不是在段落级别上工作。

Untitled

大概12.17.18号,走模拟的方案,因为要克服的障碍太多了(各种依赖),最终在费劲修复了ExANE的一个bug之后还是选择换一个fuzz方案,

frida方案的优势和劣势

对于服务器、防火墙这种东西来说,配置本来就高,frida模式可以肆无忌惮的去fuzz
但是对于手机来说,内存就很成问题,虽然未来的趋势肯定是内存越来越高,Android系统的稳定性也是一大问题

但是要用frida-fuzz,部署环境得是支持frida_server的机器,而且还得能跑AFL,像Android部署就支持frida_server,只不过部署AFL++时要克服一下共享内存和信号量的问题。

总的来说,不需要模拟的原生态fuzz本身就减少了很大的工作量了。

安卓Native层共享库fuzzing技术思路及实践 - 掘金 (juejin.cn)

这篇21 2月份发的文章其实就已经在讨论Android fuzz的事了,现在已经两年了。。。只不过其是去fuzz的纯Native库,

这里fuzz的目标入口大概分3个等级

  • 纯Native代码,不涉及Java虚拟机环境
  • JNI函数,设计JNIEnv,需要一定的和Java虚拟机交互的能力
  • 直接调用Java函数,需要完全的Java虚拟机Hook能力

用frida去fuzz,理论上这三个目标入口等级都是支持的,只不过从后期的实践效果来看,越往下越不稳定,但是辅助逆向工作量越小

AFLplusplus/frida_mode at stable · AFLplusplus/AFLplusplus (github.com)

AFL++也引入了fridamode,这个后面也有分析,一些资料

AFLplusplus/Scripting.md at stable · AFLplusplus/AFLplusplus (github.com)

Blackbox Fuzzing #4: New AFL++ FRIDA mode, How it Perform against QEMU mode?

Android漏洞挖掘资料

Android Security Acknowledgements | Android Open Source Project

Android的漏洞致谢列表,最好的研究资料。

研究root的资料 —- DirtyPipe在Android上的移植

https://github.com/polygraphene/DirtyPipe-Android

字节的污点分析工具

https://github.com/bytedance/appshark

WebView漏洞学习

Android security checklist: WebView | Oversecured Blog

https://github.com/idhyt/AndroidFridaFuzz

这个也非常关键,三星的漏洞列表

awesome-android-security/Samsung_Security_Other_Updates.csv at main · NetKingJ/awesome-android-security (github.com)

三星的SRC,以及漏洞认证分类

Security Reporting | Samsung Mobile Security

Frida_mode的核心是stalker,下面分析一下stalker

stalker

Stalker | Frida • A world-class dynamic instrumentation toolkit

stalker是一个动态插桩工具,允许对x86、x64、arm32、arm64进行动态插桩

Anatomy of a code tracer | by Ole André Vadla Ravnås | Medium

大胡子最初写的关于stalker的介绍

stalker源自于大胡子急需一个tracer
软件断点会修改内存,硬件断点也很容易被检测到,基于调试器去实现,每次trap都会消耗大量的资源,所以灵活粒度很关键,其思想就是把原来要执行的代码拷贝出来,然后在副本上加上各种log桩点,这样原先执行的反调试和校验逻辑可以正常执行,而且我们也可以粒度灵活

只对call感兴趣就只在call周围加,意味着可以根据是需要添加运行时开销

这个是实现起来也不简单,首先各种指令集插代码就和很麻烦,Capstone反编译器缓解了一部分问题,而且许多代码是位置相关的,需要调整

stalker的设计是一个基本块一个基本块的翻译,一个基本块执行完,就返回到引擎,计算分支信息,翻译下一个基本块去执行,这样肯定代价很大,一个优化策略就是重用已经编译的代码

另外像jmp+56这种,我们可以直接patch成直接跳转,但是这种修改必须谨慎,因为有些程序是自修改的,因此需要引入一个trust-threshold参数,

stalker的官方文档,其中很多内容暂时都用不上,因为涉及到stalker的底层实现,挺复杂的

Stalker | Frida • A world-class dynamic instrumentation toolkit

Transform

Transform是用来产生插桩代码的,默认的Transform长这个样

Untitled

可以看到默认的Transform一次遍历一条指令,Keep就是指令保持原样

Transform是在gum_exec_ctx_obtain_block_for调用的,也即执行到一个基本块时调用用户定义的transform

控制流跑到Stalker引擎时会保存运行上下文,stalker会存储插桩指令在slabs里面

Untitled

新版本的stalker好像已经分code_slab和data_slab,虽然官方的doc中写的还是原来的slab

这个是在14.2中提到的

Untitled

Frida 正常的unfollow里面会调用gum_exec_ctx_free来释放内存

Frida框架层

在开发FridaFuzz的过程中,更多的还是去使用其框架层,这其中有几篇关于框架层介绍的文章

Frida Internal - Part 1: 架构、Gum 与 V8 - evilpan

Untitled

frida-gum

对各种架构指令集进行inline-hook的基础

https://github.com/frida/frida-gum

stalker的实现也在这个项目的具体后端代码里

frida-gum/gum/backend-arm64 at main · frida/frida-gum (github.com)

不知道后面再调试代码bug是不是还要详细研究这个代码

除了stalker,基于frida-gum,frida还实现了一个内存监控模块。。。其实正好是去年给HW做开发的时候我研究的东西。甚至原理跟frida的都一样

Untitled

gum-js

就说是Frida-gum很强大,但是C语言接口使用很不方便,因此有了上层frida接口的JS封装,这块还没深入研究过。

frida-core

Frida-core封装了很多系统级的东西,我们用frida-tools,其实内部都是通过python binding走的fride-core

frida-java-bridge

最最靠近我们所编写的JS的脚本的一层,发现有疑惑的API,以及奇怪的报错,可以先去这里寻找一些内部实现

frida/frida-java-bridge: Java runtime interop from Frida (github.com)

frida-server(主要是stalker的修复线)

上面说的那么多东西,特别是frida-core和frida-gum最终是以集成到frida-server中给用户呈现使用的。

当时想起来研究frida-server,是因为在测试fuzzer的过程中,总是稳定的在iter几次之后崩溃,错误如下

Untitled

这个错我一开始以为是用Java.perform才遇到的,后来改写成JNI的native调用模式,还是稳定崩溃,于是就开始长达好几天的debug,最后没办法了,才研究的frida-server的debug编译,尝试从源码中找到bug

整个bug的唯一线索就是图片中给出的bug发生的pc值,如果从adb logcat中看的话,可以看到bug发生在frida-agent中,当然也可以从fpicker前面打印的模块地址列表中,找到这个地址在哪个模块里

Untitled

编译

[原创][分享]fridaserver去特征检测以及编译-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

看雪上编译frida-server的文档还挺多的,因为逆向大佬通常需要魔改server来绕过检测

先把frida拉下来

git clone **--**recurse**-**submodules https:**//**github.com**/**frida**/**frida

配置NDK路径

export ANDROID_NDK_ROOT**=**'/opt/android-ndk-r22b'

不过现在需要的25了

直接运行make core-android-arm64就行

如果要编译debug版本带调试符号的server可以

Creating debug build · Issue #1107 · frida/frida (github.com)

Before building Frida I start by editing config.mk
to remove --strip
, so the resulting binaries have debug symbols.

编译成功的效果

Untitled

Untitled

有了调试符号,下面就介绍一下我是怎么fix这个bug的

stalker的内存bug fix

通过调试符号追溯那张图中PC对应的位置

这个错发生在 gum_exec_block_maybe_create_new_data_slab

Untitled

具体位置在这里,那就是这里分配内存失败了啊

Untitled

有时候也会发生在code_slab的分配过程中

Untitled

Untitled

简单调用栈如下,(应该是改成Java.perform主动调用上层Java函数才能得到这里的native调用栈,如果主动调用的是native函数就不会得到这个调用栈)

Untitled

gum_exec_ctx_switch_block
gum_exec_ctx_obtain_block_for
gum_exec_block_new
gum_exec_block_maybe_create_new_code_slabs

因为我们没办法调试frida-server,就只能插桩打印,找了几个打印都不行,最后直接用的_android_log_print打印到logcat里面

在new_code_slab和allocate_near中加上相关地址打印

new_code_slab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static GumCodeSlab *
gum_code_slab_new (GumExecCtx * ctx)
{
GumStalker * stalker = ctx->stalker;
gsize total_size;
GumCodeSlab * code_slab;
GumSlowSlab * slow_slab;
GumAddressSpec spec;
//g_print("gum_code_slab_new be called \n");
//g_log ("SlabDebug", G_LOG_LEVEL_INFO, "gum_code_slab_new be called \n");
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","1. gum_code_slab_new be called \n");
total_size = stalker->code_slab_size_dynamic +
stalker->slow_slab_size_dynamic;

gum_exec_ctx_compute_code_address_spec (ctx, total_size, &spec);
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","2. ctx address : %p\n",ctx);
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","3. spec address : %p\n",spec.near_address);
code_slab = gum_memory_allocate_near (&spec, total_size, stalker->page_size,
stalker->is_rwx_supported ? GUM_PAGE_RWX : GUM_PAGE_RW);
//g_print("new code_slab addr: %p\n",code_slab->slab);
//g_log ("SlabDebug", G_LOG_LEVEL_INFO, "new code_slab addr: %p\n",code_slab->slab);
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","new code_slab addr: %p\n",code_slab);
gum_code_slab_init (code_slab, stalker->code_slab_size_dynamic, total_size,
stalker->page_size);

slow_slab = gum_slab_end (&code_slab->slab);
gum_slow_slab_init (slow_slab, stalker->slow_slab_size_dynamic, 0,
stalker->page_size);

return code_slab;
}

allocate_near

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
gpointer
gum_memory_allocate_near (const GumAddressSpec * spec,
gsize size,
gsize alignment,
GumPageProtection prot)
{
gpointer suggested_base, received_base;
GumAllocNearContext ctx;
//g_print("gum_memory_allocate_near be called\n");
//g_log ("SlabDebug", G_LOG_LEVEL_INFO, "gum_memory_allocate_near be called\n");
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","4. gum_memory_allocate_near be called\n");
suggested_base = (spec != NULL) ? spec->near_address : NULL;



received_base = gum_memory_allocate (suggested_base, size, alignment, prot);
//g_print("1. gum_memory_allocate_near received_base %p\n",received_base);
//g_log ("SlabDebug", G_LOG_LEVEL_INFO, "1. gum_memory_allocate_near received_base %p\n",received_base);
if(spec != NULL)
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","8.1 spec address : %p\n",spec->near_address);
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","8.2. gum_memory_allocate_near received_base %p\n",received_base);
if(spec != NULL && !gum_address_spec_is_satisfied_by (spec, received_base))
{
__android_log_print(ANDROID_LOG_INFO,"SlabDebug","8.3 unsatisfied spec!!!!!!!!!!");
}
//delete spec
spec = NULL;
if (received_base == NULL)
return NULL;
if (spec == NULL || gum_address_spec_is_satisfied_by (spec, received_base))
return received_base;
gum_memory_free (received_base, size);

ctx.spec = spec;
ctx.size = size;
ctx.alignment = alignment;
ctx.page_size = gum_query_page_size ();
ctx.prot = prot;
ctx.result = NULL;

gum_enumerate_free_ranges (gum_try_alloc_in_range_if_near_enough, &ctx);

return ctx.result;
}

分配空间先会从gum_memory_allocate 中分配,里面就是一个mmap,如果分配条件为空或者gum_address_spec_is_satisfied_by 不满足,就会调用gum_enumerate_free_ranges遍历内存 ,从当前空闲的内存区域中分配,这里spec=NULL,就是我做的fix,后面再说,

gum_address_spec_is_satisfied_by 是判断当前分配的地址是否离spec过远

Untitled

本来alloc_near会传入一个建议分配到的地址,但是mmap要求分配的地址和返回地址不一定一样,这里我做了个实验,可以看到我传的预想地址是0x7000.结果两次mmap返回的地址都不是

Untitled

既然这里mmap addr和mmap result地址不同,那如果是这样,总有可能分配出来的大于uint32

之所以报crash,就是因为申请到的地址大于0x7ffffffff不满足条件,所以触发了下面alloc in range,结果一个都找不到,最终只能返回0,因为slab初始化时因为访问了0地址,产生了各种奇怪的0x18、0x10类的错误,其实都是结构体成员的偏移。我们用stalker插桩还必须得让其能够分配出来slab,所以这里到底为什么要加这个spec,原因不是很明白。。分配不出来怎么办,大胡子也没说,所以最终我尝试始终给spec赋值为NULL,这样其就不会检查分配出的内存的合理性,相当于取消spec检查,结果发现反而可以正常跑了。。。

老版本是没有这个spec的限制的

Untitled

直到14.2.14引入的code和data slab分离,spec也是在这时被加入的

Untitled

当时就没搞懂为什么要加这个限制,看文档疑似讨论了这个点,还是不懂

Untitled

最后只能在github上提了个issue,到现在也没人回

https://github.com/frida/frida-gum/issues/707

AFL++

本文主要使用的fpicker-android中引用AFL++版本,其Readme中介绍的是用AOSP的编译环境进行编译的,最新版本的AFL++,我还没有试过用AOSP的环境能不能编译出来,其对应的编译文件Android.bp的开头就写了因为AFL++没有团队成员用Android。。所以大概率编译会出错,不过暂时不用考虑这个问题,AFL++的变异驱动,先用fpicker-android这个版本,能用就行。

Untitled

fpicker使用的版本还是3.13

Untitled

编译

要编译AFL++,得先配置AOSP的编译环境,这个已经不知道复现多少次了

2020年安卓源码编译指南及FART脱壳机谷歌全设备镜像发布-安全客 - 安全资讯平台 (anquanke.com)

AOSP配置

原先都是刷Android8,直接下载别人下载好的源码包,现在要fuzz的是最新的12、13,只能自己把整个源码包下载下来了,还好国内有科大、清华两个源

下载源代码 | Android 开源项目 | Android Open Source Project

下载完毕后

repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest -b android-12.1.0_r1

这中间可能会经历git的错误

Untitled

这个问题修复,其让我增大http的缓存,但实际上他给的buffer大小也就500MB,jdk11我下载发现是578MB,所以我们再扩大一点buffer
git config –global http.postBuffer 10485760000

如果还报错,可以把错误的对象分支删掉。。要是还有别的错误可以问下chatGPT

(46条消息) fatal: bad object refs/remotes/origin/xxx 解决方法_舜岳的博客-CSDN博客

afl++编译

AFL++的编译非常简单,只需要把fpicker里跟的AFL++拖到AOSP的根目录

先运行source build/envsetup配置下环境

然后lunch选第二项(arm64-eng)

最后直接mmm afl++即可,编译成功会给出afl++产生在哪个文件夹里

这里编译用的应该就是Android.bp,Android的编译系统

Soong Build System | Android Open Source Project

Untitled

但是没看出bp哪里有特别的地方,

Untitled

Untitled

编译成功的效果

Untitled

Untitled

Untitled

FASAN,常规的Address Sanitizer basics是用DT_NEED把 ASAN的库链接到目标程序里,主要逻辑包括设置shadow memory,提供内存替代函数的实现。
主要的替换就是malloc、free、memcpy等

testcase

一开始疑惑frida-mode怎么测,后来发现frida-mode里面有示例的testcase

AFLplusplus/GNUmakefile at stable · AFLplusplus/AFLplusplus (github.com)

以一个freetype的MakeFile看一下frida_mode是怎么fuzz的

下载freetype

Untitled

libfuzzer的Harness

就是包裹一层main函数

Untitled

源码分析

2-11

参数解析

i 设置输入目录,指定-i- 就是恢复模式

1
2
3
4
5
6
7
8
9
case 'i':                                                /* input dir */

if (afl->in_dir) { FATAL("Multiple -i options not supported"); }
if (optarg == NULL) { FATAL("Invalid -i option (got NULL)."); }
afl->in_dir = optarg;

if (!strcmp(afl->in_dir, "-")) { afl->in_place_resume = 1; }

break;

Resume模式

搜索_resume

处理resume的逻辑在这里

就是简单把原来的queue重命名为_resume

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/* Delete fuzzer output directory if we recognize it as ours, if the fuzzer
is not currently running, and if the last run time isn't too great.
Resume fuzzing if `-` is set as in_dir or if AFL_AUTORESUME is set */

static void handle_existing_out_dir(afl_state_t *afl) {
if (afl->in_place_resume) {

u8 *orig_q = alloc_printf("%s/queue", afl->out_dir);

afl->in_dir = alloc_printf("%s/_resume", afl->out_dir);
rename(orig_q, afl->in_dir); /* Ignore errors */

OKF("Output directory exists, will attempt session resume.");

ck_free(orig_q);

in_place_resume就是输入参数-in-时设置的

Untitled

而Handle_exiting_out_dir是在配置输出目录时设置的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void setup_dirs_fds(afl_state_t *afl) {

u8 *tmp;

ACTF("Setting up output directories...");

if (afl->sync_id && mkdir(afl->sync_dir, 0700) && errno != EEXIST) {

PFATAL("Unable to create '%s'", afl->sync_dir);

}

if (mkdir(afl->out_dir, 0700)) {

if (errno != EEXIST) { PFATAL("Unable to create '%s'", afl->out_dir); }

handle_existing_out_dir(afl);

} else {

然后看一下queue目录是怎么生成的

fuzz刚开始时候的输出

Untitled

首先是scan,然后是load,然后是create hard links,就在pivot_inputs

Pivot-queue实际就是遍历queue_paths,然后在queue中创建对应项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Create hard links for input test cases in the output directory, choosing
good names and pivoting accordingly. */

void pivot_inputs(afl_state_t *afl) {

struct queue_entry *q;
u32 id = 0, i;

ACTF("Creating hard links for all input files...");

for (i = 0; i < afl->queued_paths && likely(afl->queue_buf[i]); i++) {

q = afl->queue_buf[i];

nfn = alloc_printf("%s/queue/id:%06u,time:0,orig:%s", afl->out_dir, id,
use_name);

#else

nfn = alloc_printf("%s/queue/id_%06u", afl->out_dir, id);

#endif /* ^!SIMPLE_FILES */

}

/* Pivot to the new queue entry. */

link_or_copy(q->fname, nfn);

创建hard_link的函数就是Link_or_copy ,就是拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void link_or_copy(u8 *old_path, u8 *new_path) {

s32 i = link(old_path, new_path);
s32 sfd, dfd;
u8 *tmp;

if (!i) { return; }

sfd = open(old_path, O_RDONLY);
if (sfd < 0) { PFATAL("Unable to open '%s'", old_path); }

dfd = open(new_path, O_WRONLY | O_CREAT | O_EXCL, DEFAULT_PERMISSION);
if (dfd < 0) { PFATAL("Unable to create '%s'", new_path); }

tmp = ck_alloc(64 * 1024);

while ((i = read(sfd, tmp, 64 * 1024)) > 0) {

ck_write(dfd, tmp, i, new_path);

}

if (i < 0) { PFATAL("read() failed"); }

ck_free(tmp);
close(sfd);
close(dfd);

}

正常创建queue,在开局scan input_dir时会创建一次,

1
2
3
4
5
6
7
8
/* Read all testcases from the input directory, then queue them for testing.
Called at startup. */

void read_testcases(afl_state_t *afl, u8 *directory) {
ACTF("Scanning '%s'...", dir);

add_to_queue(afl, fn2, st.st_size >= MAX_FILE ? MAX_FILE : st.st_size,
passed_det);

之后就是save_if_intersting ,会把感兴趣的样本也放进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Check if the result of an execve() during routine fuzzing is interesting,
save or queue the input test case for further analysis if so. Returns 1 if
entry is saved, 0 otherwise. */

u8 __attribute__((hot))
save_if_interesting(afl_state_t *afl, void *mem, u32 len, u8 fault) {

if (unlikely(len == 0)) { return 0; }

u8 *queue_fn = "";
u8 new_bits = '\0';
s32 fd;
u8 keeping = 0, res, classified = 0;
u64 cksum = 0;
#ifndef SIMPLE_FILES

queue_fn = alloc_printf(
"%s/queue/id:%06u,%s", afl->out_dir, afl->queued_paths,
describe_op(afl, new_bits, NAME_MAX - strlen("id:000000,")));

#else

queue_fn =
alloc_printf("%s/queue/id_%06u", afl->out_dir, afl->queued_paths);

#endif /* ^!SIMPLE_FILES */
fd = open(queue_fn, O_WRONLY | O_CREAT | O_EXCL, DEFAULT_PERMISSION);
if (unlikely(fd < 0)) { PFATAL("Unable to create '%s'", queue_fn); }
ck_write(fd, mem, len, queue_fn);
close(fd);
add_to_queue(afl, queue_fn, len, 0);

共享内存的创建

AFL通过共享内存来和Forkserver共享覆盖率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

/* Configure shared memory.
Returns a pointer to shm->map for ease of use.
*/
//add COMMAP
#define COMMAP_SIZE 0xA0000
#define COMMAP_ENV_MAP "COMMAP_ENV_MAP"
#define EXEC_ENV_MAP "EXEC_ENV_MAP"
#define ITER_ENV_MAP "ITER_ENV_MAP"
u8 *afl_shm_init(sharedmem_t *shm, size_t map_size,
unsigned char non_instrumented_mode) {
shm->map = NULL;
shm->cmp_map = NULL;
shm->commap = NULL;
u8 *shm_str;

shm->shm_id =
shmget(IPC_PRIVATE, map_size, IPC_CREAT | IPC_EXCL | DEFAULT_PERMISSION);
printf("shm_id %i\n",shm->shm_id);
if (shm->shm_id < 0) { PFATAL("shmget() failed"); }

if (shm->cmplog_mode) {

shm->cmplog_shm_id = shmget(IPC_PRIVATE, sizeof(struct cmp_map),
IPC_CREAT | IPC_EXCL | DEFAULT_PERMISSION);

if (shm->cmplog_shm_id < 0) {

shmctl(shm->shm_id, IPC_RMID, NULL); // do not leak shmem
PFATAL("shmget() failed");

}

}

setenv(SHM_ENV_VAR, shm_str, 1);
setenv(CMPLOG_SHM_ENV_VAR, shm_str, 1);

shm->map = shmat(shm->shm_id, NULL, 0);

if (shm->cmplog_mode) {

shm->cmp_map = shmat(shm->cmplog_shm_id, NULL, 0);

map-size就是65536,shmget打开共享内存,如果开了CMP_LOG,下面还会打开一个cmplog_shm_id

这个id会被设置到环境变量中,最后会通过shmat将两个id对应的shm映射到进程中

核心Fuzz逻辑

解析参数,分析in目录的种子

perform_dry_run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

/* Perform dry run of all test cases to confirm that the app is working as
expected. This is done only for the initial inputs, and only once. */

void perform_dry_run(afl_state_t *afl) {

struct queue_entry *q;
u32 cal_failures = 0, idx;
u8 * skip_crashes = afl->afl_env.afl_skip_crashes;
u8 * use_mem;
//dry每个输入
DACTF("[debug 1] perform_dry_run: queued_paths,%d.", afl->queued_paths);
for (idx = 0; idx < afl->queued_paths; idx++) {

q = afl->queue_buf[idx];
if (unlikely(!q || q->disabled)) { continue; }

u8 res;
s32 fd;

AFL会维护一个当前的所有种子,保存在队列Queue_paths,dry_run会遍历整个种子队列,都运行一遍看看其是否能正常使用,不会有crash、timeout这种,dry到哪了会打印出来

1
2
3
u8 *fn = strrchr(q->fname, '/') + 1;

ACTF("Attempting dry run with '%s'...", fn);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

fd = open(q->fname, O_RDONLY);
if (fd < 0) { PFATAL("Unable to open '%s'", q->fname); }

u32 read_len = MIN(q->len, (u32)MAX_FILE);
use_mem = afl_realloc(AFL_BUF_PARAM(in), read_len);
if (read(fd, use_mem, read_len) != (ssize_t)read_len) {

FATAL("Short read from '%s'", q->fname);

}

close(fd);

res = calibrate_case(afl, q, use_mem, 0, 1);

具体dry是通过calibrate_case函数,这里面会运行多次,以确保是样本是没问题的,默认就是8次

1
2
3
4
5
6
7
8
9
//calibrate_case 8次,确保输入是没问题的
for (afl->stage_cur = 0; afl->stage_cur < afl->stage_max; ++afl->stage_cur) {

u64 cksum;
//写入testcases
write_to_testcase(afl, use_mem, q->len);

fault = fuzz_run_target(afl, &afl->fsrv, use_tmout);
DACTF("[debug 1] calibrate_case: fuzz_run_target.");

AFL++每次进行fuzz前都会先对样本queue进行依次dry(就上面分析的)

这个正常是没问题的,但是因为我们针对glib老是崩溃的问题,用了fuzz启动器不停的启动fuzz,这就导致,后期resume模式下,queue越来越长,这里dry的时间就会越来长,一种解决方式是可以减少stage_max到1次,而且按AFL,也必须运行一次,因为要载入trace_bits…

保存状态

和Fpicker的通讯

2-16

Libdislocator

Fpicker

fpicker: Fuzzing with Frida – Insinuator.net

最初的介绍文档

fpicker的Android移植

虽然fpicker最初写的支持Android、Linux、Macos,但是其实对Android的支持非常有限

Untitled

从源代码里可以看到用USB时,不支持共享内存,也不支持AFL

直接搜Android是能找到很多和我一样的疑问的,如何使fpicker运行在Android上

Untitled

讨论的非常多

marcinguy/fpicker at android-port-forward (github.com)

有许多讨论,不过作者都没有合并,这个是让fpicker支持网络模式的

Fuzzing in afl++ mode on android device · Issue #5 · ttdennis/fpicker (github.com)

这个是讨论怎么支持afl++在Android模式上

作者说因为fpicker需要AFL++的共享内存,如果要跨USB使用,就得用standalone mode,然后自己实现一个更好的mutator,否则就得用fpicker自己实现的random变异

因为Android上没有全局共享内存的概念,

Untitled

当时本来觉得研究就此无望了,结果这老哥隔了一年,就在我发现这个玩意的上个月把自己的fpicker移植Android的成果发出来了

Untitled

Untitled

https://github.com/marcinguy/fpicker-aflpp-android/

其瑕疵还是不少,但是就此开启了我后面两个月的debug。。。。

之所以作者能把fpicker移植成功,是因为其将共享内存的相关代码都换成了android-shmem的模拟实现上

源码分析

fpicker一共就三个文件,fpicker.c 、fp_communicate.c 、fp_afl_mode.c

fpicker.c是负责fpicker的启动和一些通用类

fp_communicate.c是负责和harness Agent的交流

fp_afl_mode.c是负责和AFL交流

fpicker的main主要下面几个过程

解析参数

1
fuzzer_state_t *fstate = parse_args(argc, argv);

fstate是包含整个fstate状态的结构体,其中fconfig记录了当前解析参数得到各项配置

具体一些选项可以看github

https://github.com/ttdennis/fpicker

我们主要用的几个选项就是afl,in_process、spawn,一个样例启动参数如下

1
AFL_DEBUG=1 AFL_SKIP_BIN_CHECK=1 LD_PRELOAD=/data/local/tmp/libandroid-shmem.so AFL_NO_AFFINITY=1 ./afl-fuzz -i- -o out -- ./fpicker -v  -u send --fuzzer-mode afl -e spawn -p com.zzr.testfuzz -f ./agent.js -t 9999 com.zzr.testfuzz

配置和AFL进行覆盖率通信的SHM

1
2
3
4
5
6
7
8
9
if (fstate->config->fuzzer_mode == FUZZER_MODE_AFL) {
fstate->shm_id = getenv(SHM_ENV_VAR);
// if SHM_ENV_VAR does not exist we're not running in AFL (or there's some other problem)
if (!fstate->shm_id) {
plog("[!] " SHM_ENV_VAR " not defined!\n");
return 1;
}
plog("[*] SHM_ENV_VAR = %s\n", fstate->shm_id);
}

这个shm会向下传递给stalker,其会在执行到基本块时写进这个共享内存

配置和Harness交流shm

1
2
3
4
5
6
7
fstate->config->verbose = true;
if (fstate->config->communication_mode == COMMUNICATION_MODE_SHM) {
plog("[*] create_communication_map \n");
//create_communication_map(fstate);
//open_communication_map(fstate);
}
open_communication_map(fstate);

不知道为什么在fpicker中没办法创建共享内存,因为LD_PRELOAD=/data/local/tmp/libandroid-shmem.so打在AFL的启动前面的,可能没办法hook到fpicker这个子进程上,所以我采用的办法和覆盖率SHM时一样的,就是由AFL创建,然后fpicker再打开使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#define COMMAP_ENV_MAP "COMMAP_ENV_MAP"
#define EXEC_ENV_MAP "EXEC_ENV_MAP"
#define ITER_ENV_MAP "ITER_ENV_MAP"
void open_communication_map(fuzzer_state_t *fstate) {
char *shm_id = malloc(128);
char *commap_id_str = getenv(COMMAP_ENV_MAP);
// if SHM_ENV_VAR does not exist we're not running in AFL (or there's some other problem)
if (!commap_id_str) {
plog("[!] " COMMAP_ENV_MAP " not defined!\n");
do_exit(fstate);;
}
plog("[*] COMMAP_ENV_MAP = %s\n", commap_id_str);
unsigned int commap_id = atoi(commap_id_str);
//核心的传输入数据的共享内存
//int commap_id = shmget(IPC_PRIVATE, COMMAP_SIZE, IPC_CREAT | IPC_EXCL | 0644);
plog("[*] Created commap = %d\n", commap_id);
if(commap_id == -1)
plog("%s\n", strerror(errno));
snprintf(shm_id, 128, "%d", commap_id);
fstate->commap_id = shm_id;
fstate->commap = shmat(commap_id, NULL, 0);
plog("[*] commap_addr = %lx\n", fstate->commap);

//open exec and iter map
//char *exec_id = malloc(128);
char *exec_map_id_str = getenv(EXEC_ENV_MAP);
//char *iter_id = malloc(128);
char *iter_map_id_str = getenv(ITER_ENV_MAP);
if (!exec_map_id_str || !iter_map_id_str) {
plog("[!] " EXEC_ENV_MAP " not defined!\n");
do_exit(fstate);;
}
plog("[*] EXEC_ENV_MAP = %s\n", exec_map_id_str);
plog("[*] ITER_ENV_MAP = %s\n", iter_map_id_str);
unsigned int exec_id = atoi(exec_map_id_str);
unsigned int iter_id = atoi(iter_map_id_str);

if(exec_id == -1 || iter_id == -1)
plog("%s\n", strerror(errno));
fstate->exec_id = exec_map_id_str;
fstate->exec_sem = shmat(exec_id, NULL, 0);
plog("[*] exec_addr = %lx\n",fstate->exec_sem );

fstate->iter_id = iter_map_id_str;
fstate->iteration_sem = shmat(iter_id, NULL, 0);
plog("[*] iter_addr = %lx\n",fstate->iteration_sem );
*(fstate->exec_sem) = 0;
*(fstate->iteration_sem) = 0;
}

因为Android不支持信号量,所以exec_sem和iteration_sem两个信号量也是用共享内存实现的,一开始担心不是原子操作会不会出同步问题,后来想想,两个共享内存负责进出还好,不会出现问题。。

frida加载

首先是frida初始化

1
frida_init()

这个就是直接调用core里面的API,说起来frida-core下载解压后,其API都在frida-core.h里面

创建设备管理器,遍历设备,得到当前的设备数目

1
2
3
4
5
6
7
manager = frida_device_manager_new();
devices = frida_device_manager_enumerate_devices_sync(manager, NULL, &error);
num_devices = frida_device_list_size(devices);
//保存device到fstate中
fstate->frida_device = device;

session = spawn_or_attach(fstate);

经过测试,只有用Remote类型连上frida-server才能成功进行接下来的操作,

得到device需要启动并附加到device上的进程中,因为我们要考虑到fuzz过程中crash、以及目标app突然退出的问题,所以必须采用spawn模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FridaSession *spawn_or_attach(fuzzer_state_t *fstate) {
fuzzer_config_t *config = fstate->config;

FridaSession *session;
GError *error = NULL;
pid_t target_pid = 0;
FridaDevice *device = fstate->frida_device;

if (config->exec_mode == EXEC_MODE_SPAWN) {
plog("[*] Trying to spawn %s on device %s\n", config->process_name, frida_device_get_name(device));
plog("[*] spawn argc %d argv %s\n", config->spawn_argc, config->spawn_argv[0]);
FridaSpawnOptions *spawn_options = frida_spawn_options_new();
if (config->spawn_argc > 1) {
plog("[*] set spawn options\n");
frida_spawn_options_set_argv(spawn_options, config->spawn_argv, config->spawn_argc);
}

target_pid = frida_device_spawn_sync(device, config->process_name, spawn_options, NULL, &error);
g_object_unref(spawn_options);
if (error) {
plog("[!] Failed to spawn %s %s\n", config->process_name,error->message);
return NULL;
}

plog("[*] Spawned %s with PID %d\n", config->process_name, target_pid);
fstate->target_pid = target_pid;
plog("[*] test processName %s\n", config->process_name);
}

目前版本的frida,spawn启动,只要process_name是app的包名就行,其会自动启动app,其实当时我还想过一个折衷方案,就是写一个c程序用pm去启动,再用attach去附加,不过后来摸索出来直接用frida去spawn的API写法,这里spawn完成会返回pid

1
session = frida_device_attach_sync(device, target_pid, FRIDA_REALM_NATIVE, NULL, &error);

紧接着调用attach把frida挂上去,得到session,最后还有两个关键的步骤,因为frida spawn之后,程序是卡在入口的,所以这里要resume,让其恢复运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
frida_device_resume_sync(fstate->frida_device,fstate->target_pid,NULL,&error);
if (error != NULL) {
//plog("[!] Failed to attach to process %s on frida device %s (%s)\n", config->process_name, frida_device_get_name(device), error->message);
plog("[*] %s\n",error->message);
g_error_free(error);
return NULL;
}

frida_session_resume_sync(session,NULL,&error);
if (error != NULL) {
//plog("[!] Failed to attach to process %s on frida device %s (%s)\n", config->process_name, frida_device_get_name(device), error->message);
plog("[*] %s\n",error->message);
g_error_free(error);
return NULL;
}

编译

Harness Agent

Prepare阶段

有时候fuzz的不是静态方法,就需要在prepare阶段找一下对象实例

1
2
3
4
5
fuzz(payload, len) {
if(this.taget_object == null)
{
this.prepare();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function () {   
Java.choose("com.zzr.testfuzz.MainActivity",{
onMatch : function(instance){
self.debug_log("find targetObject!!");
// something to do...
MainActivity_obj = Java.retain(instance);
},

onComplete: function(){
self.debug_log("compelete!!!");
}
});

});

数组类型

用frida的API,Java.array,这个效率奇低,不知道为什么,因为经过了Java虚拟机吗。。

但是如果我们想fuzz的就是Java层的API,那就只能用这种方式,除非再往下逆向到JNI层

Scudo

新版Android的Scudo确实给我带来了许多麻烦。。

内存标签

Andriod Native | 采样型内存调试工具GWP-ASan - 掘金 (juejin.cn)

Untitled

Untitled

Scudo分配的内存会在高位上打一个tag,所以后面我们做malloc hook的时候,就要判断下这个tag,

Scudo crash流程分析

2-27

Fuzz测试

testfuzz

自己写的测试程序,写了几个简单的crash的条件分支

访问0地址和堆溢出

Untitled

要注意堆溢出并不一定会立马产生crash,特别是如果没有加ASAN的情况下,因此在我们这个测试场景下,延迟的crash只是因为堆的overflow不能立马造成crash,后面一系列继续的堆操作才导致的crash,如果这个继续的堆操作刚好位于下次的执行就会出现延迟crash的问题,
因为我们还没研究重置堆环境这个问题。

所以这个fuzz,不算太精准的fuzz

Fuzz启动器

其实可能也是上面介绍的堆的问题,fuzz很不稳定,大概fuzz到一定次数就会报一个glib的错误,至今还没解决,所以我只能采取一个替代方案,就是写一个fuzz启动器,监测到fuzz结束就重启,

主循环如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
int main() {
int pipefd[2];
pid_t pid;

FILE *fp = fopen("fuzz_daemon_output.txt", "w");

// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}

int first_iter = 1;
while (1) {
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程中启动子程序,并将其输出重定向到管道中
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);

char *envp[] = {
"AFL_DEBUG=1",
"AFL_SKIP_BIN_CHECK=1",
"LD_PRELOAD=/data/local/tmp/libandroid-shmem.so",
"AFL_NO_AFFINITY=1",
NULL
};
if(first_iter){

execle("./afl-fuzz", "afl-fuzz", "-i","in", "-o", "out", "--", "./fpicker", "-v", "-u", "send", "--fuzzer-mode", "afl", "-e", "spawn", "-p", "com.zzr.testfuzz", "-f", "./agent.js", "-t", "9999", "com.zzr.testfuzz", NULL, envp);
perror("execl");

}else
{
execle("./afl-fuzz", "afl-fuzz", "-i-", "-o", "out", "--", "./fpicker", "-v", "-u", "send", "--fuzzer-mode", "afl", "-e", "spawn", "-p", "com.zzr.testfuzz", "-f", "./agent.js", "-t", "9999", "com.zzr.testfuzz", NULL, envp);
perror("execl");
}
//execl("/path/to/your/program", "/path/to/your/program", NULL);
//execle("./test_fuzz_deamon", "test_fuzz_deamon", NULL, envp);

return 1;
} else {
// 父进程中等待子进程结束,并从管道中读取子程序的输出并打印
first_iter = 0;
close(pipefd[1]);
char buf[BUF_SIZE];
int status;
while (waitpid(pid, &status, WNOHANG) != pid) {
int bytes_read = read(pipefd[0], buf, BUF_SIZE);
if (bytes_read > 0) {
write(STDOUT_FILENO, buf, bytes_read);
}
}
int findit = findCrashInfo("crash_in_try");
if(findit)
{
//printf("find a crash\n");
fprintf(fp, "find a crash\n");

findit = findCrashInfo("g_array_append_vals");
if(findit)
//printf("but is g_array_append_vals");
fprintf(fp, "but is g_array_append_vals\n");
fflush(fp);
}
deleteFuzzlog();
}
}
fclose(fp);
return 0;
}

其实可以看作是一个daemon进程,只不过第一次启动时参数用的是正常的”-i”,”in”

第二次启动时用的参数是”-i-“

这里每次一轮fuzz结束时,会找一下crash的原因,如果是g_array_append_vals,就说明是glib崩溃,否则说明确实找到了crash

这里用到的两个子函数,负责删除每一轮产生的fuzz.log以及在fuzz.log中查找字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int deleteFuzzlog(){
const char* filename = "fuzz.log";

// 删除指定的文件
if (unlink(filename) == -1) {
perror("unlink");
return 1;
}

printf("File %s has been deleted.\n", filename);

return 0;
}

int findCrashInfo(const char* const_to_find){
const char* filename = "fuzz.log";
//const char* target_str = "crash_in_try";
const char* target_str = const_to_find;
char line[1024];
int findit = 0;
// 打开文件
FILE* fp = fopen(filename, "r");
if (!fp) {
perror("fopen");
return 1;
}

// 逐行读取文件内容,并查找目标字符串
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, target_str)) {
printf("File %s contains string '%s'\n", filename, target_str);
findit = 1;
break;
}
}

// 关闭文件
fclose(fp);

return findit;
}

这里还有个小细节

这个first_iter=0,不能写在子进程中,这样设置,不会同步到父进程里,所以下次first_iter还是等于1

屏幕唤醒器

。。本来afl在后台跑应该是支持息屏的。。但是息屏app好像就不跑了,导致fuzz这边也会卡顿,因此又写了一个息屏唤醒器,每5分钟唤醒一下,因为三星手机没办法设置一直保持唤醒,所以只要这里唤醒的间隔大于,手机最大设置的唤醒间隔就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_alarm(int sig) {
system("adb shell input keyevent 26");
sleep(1);
system("adb shell input keyevent 26");
alarm(300); // 设置下一次定时器
}

int main() {
signal(SIGALRM, handle_alarm); // 注册信号处理函数
alarm(300); // 设置第一次定时器
while (1) {
pause(); // 等待信号
}
return 0;
}

用的是adb命令,,这里要发两次keyevent,一次是息屏,第二次是开屏

libpng

自己的testFuzz基本算是测试完成了,找了个libpng测试一下

julienr/libpng-android at stable (github.com)

Untitled

直接把文件全拉过来,只不过这里编译需要zlib,不过Android NDK有zlib,find然后targetlink就行

Untitled

测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Java_com_zzr_testfuzz_MainActivity_testlibpng(JNIEnv *env, jclass clazz) {
// TODO: implement testlibpng()
__android_log_print(ANDROID_LOG_INFO,"testFuzz","libpng in ");
FILE *png_file = fopen("/data/local/tmp/test_libpng.png", "rb");
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info_ptr = png_create_info_struct(png_ptr);
png_init_io(png_ptr, png_file);
png_read_info(png_ptr, info_ptr);

int width = png_get_image_width(png_ptr, info_ptr);

int height = png_get_image_height(png_ptr, info_ptr);

__android_log_print(ANDROID_LOG_INFO,"testFuzz","libpng width: %d,height:%d",width,height);
png_byte color_type = png_get_color_type(png_ptr, info_ptr);
png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);

png_bytep *row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * height);
for (int y = 0; y < height; y++) {
row_pointers[y] = (png_byte*) malloc(png_get_rowbytes(png_ptr, info_ptr));
}

png_read_image(png_ptr, row_pointers);

fclose(png_file);
for (int y = 0; y < height; y++) {
free(row_pointers[y]);
}
free(row_pointers);
__android_log_print(ANDROID_LOG_INFO,"testFuzz","libpng success");
}

其输入是读取的“/data/local/tmp/test_libpng.png”

所以frida脚本fuzzInternal里传递输入要写入到这个文件里

这里加上libpng的打印了,所以如果获取成功,这里是可以看到打印的